/*
* Copyright (c) 2010 The Turing Project
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* Except as contained in this notice, the name(s) of the above
* copyright holders shall not be used in advertising or otherwise to
* promote the sale, use or other dealings in this Software without prior
* written authorization.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
 */

package simulator;
import java.awt.Graphics2D;

/**
 * The Model class is used to apply the physical and neural mechanics to the
 * innanimate members of the Construct class.
 */
public class Model {
    
    private int _nNodes;
    private int _nEdges;
    private Node _nodes[] = new Node[10000];
    private Edge _edges[] = new Edge[10000];
    
    private Double _modelTime;
    private Double _timeIncrement;
    private Double _maximumTimeIncrement;
    private Double _frictionCoeficient;
    private Double _maximumTravelDistance;

    private Double _groundLevel;
    
    private Vector3D _gravitationalForce = new Vector3D();

    private Trace _traces[] = new Trace[100];
    private Double _lastTraceUpdate;
    private Double _traceInterval;

    public Model() {
        _nNodes = 0;
        _nEdges = 0;
        _modelTime = 0.0;
        _timeIncrement = 0.01;
        _maximumTimeIncrement = 0.1;
        _frictionCoeficient = 0.4;
        _maximumTravelDistance = 0.1;
        _gravitationalForce.cartesianSet(0.0, 0.0, 9.8);
        _groundLevel = -80.0;
        _lastTraceUpdate = 0.0;
        _traceInterval = 0.01;
    }

    /**
     * Sets tha maximum distance that any nodeID can travel in a given instance
     * of the simulation.  It is imortant that this distance be less than the
     * radius of the smallest nodeID.
     * @param maximumTravelDistance
     */
    public void setMaximumTravelDistance (Double maximumTravelDistance) {
        _maximumTravelDistance = maximumTravelDistance;
    }

    /**
     * Sets the maximum amount the model time will be incremented during a
     * single step in the animation.
     * @param maximumTimeIncrement
     * @see #getMaximumTimeIncrement()
     */
    public void setMaximumTimeIncrement (Double maximumTimeIncrement) {
        _maximumTimeIncrement = maximumTimeIncrement;
    }

    /**
     * Returns the maximum amount the model time will be incremented during a
     * single step in the animation.
     * @return
     * @see #setMaximumTimeIncrement(java.lang.Double)
     */
    public Double getMaximumTimeIncrement () {
       return _maximumTimeIncrement;
    }

    /**
     * Reports the total number of nodes in the model.
     * @return
     */
    public int countNodes() {
        return _nNodes;
    }

    /**
     * Reports the total number of edges in the model.
     * @return
     */
    public int countEdges() {
        return _nEdges;
    }

    /**
     * Imports a Construct class or Construct subclass into the Model class
     * so that it can be managed by the physical and neural functions embedded in the Model class.
     * @param construct The Construct or Construct subclass to be added to the model.
     * @see Model
     */
    public void addConstruct(Construct construct) {

        for(int i = 0; i < construct.countEdges(); i++) {
            _edges[_nEdges] = construct.fetchEdge(i);
            _nEdges += 1;
        }

        for(int i = 0; i < construct.countNodes(); i++) {
            _nodes[_nNodes] = construct.fetchNode(i);
            _nNodes += 1;
        }

        defineTraces();
    }

     /**
     * Returns the coordinate (0, 1, or 2) of the specified nodeID (0 or 1) of the specified
     * model edge.
     * @param edge
     * @param nodeID
     * @param coordinate
     * @return
     */
    public Double getEdgeCoordinate(int edge, int node, int coordinate){
        return _edges[edge].getNodeCoordinate(node, coordinate);
    }

    /**
     * Returns the specified coordinate (0, 1, or 2) of the model nodeID.
     * @param nodeID
     * @param coordinate
     * @return
     */
    public Double getNodeCoordinate(int nodeID, int coordinate){
        Double value = 0.0;
        if (nodeID < _nNodes) {
           value = _nodes[nodeID].getCoordinate(coordinate);
        }
        return value;
    }

    /**
     * Returns the radius of the specified node.
     * @param nodeID
     * @return
     */
    public double getNodeRadius (int nodeID) {
        return _nodes[nodeID].getRadius();
    }

    /**
     * Returns the diameter of the specified node.
     * @param nodeID
     * @return
     */
    public double getNodeDiameter (int nodeID) {
        return _nodes[nodeID].getRadius() * 2.0;
    }

    /**
     * Computes all the forces acting on all the nodes and determines the new
     * velocity for each nodeID based on timeIncrement.  The method is false if
     * at lease one of these velocities results in a nodeID traveling further than
     * the class's maximumTravelDistance in the specified timeIncrement.
     * @param timeIncrement
     * @return
     */
    private void applyForces() {

       for (int i = 0; i < _nEdges; i ++) {
          _edges[i].applyEdgeForce(_timeIncrement);
       }

       for (int i = 0; i < _nNodes; i ++) {
           for (int j = 0; j < _nNodes; j ++) {
             _nodes[i].applyCollisionForce(_nodes[j], _timeIncrement);
           }
       }

       for (int i = 0; i < _nNodes; i ++) {
           _nodes[i].applyFriction(_frictionCoeficient, _timeIncrement);
           _nodes[i].applyNegativeForce(_gravitationalForce, _timeIncrement);
           _nodes[i].applyGroundCollisionForce(_groundLevel);
       }

    }

    /**
     * Defines traces for those items and measurements that have requested traces.
     * Traces are generally requested by activating a boolean flag in a
     * particular items, such as a node or an edge, to indicate that one of the
     * item's values should be tracked.
     * @see simulator.Node.setTraceVelocity()
     */
    public void defineTraces() {
        updateTraces();
        int currentTrace = 0;
        for (int i = 0; i < _nNodes; i ++) {
            if (_nodes[i].traceVelocity()) {
                _traces[currentTrace] = new Trace();
                _traces[currentTrace].define("Node", i, "Velocity", "m/s");
                currentTrace += 1;
            }
        }
    }

    /**
     * Replots all of the traces.
     * @param g2d
     */
    public void redrawTraces(Graphics2D g2d) {
        updateTraces();
        int currentTrace = 0;
        for (int i = 0; i < _nNodes; i ++) {
            if (_nodes[i].traceVelocity()) {
                _traces[currentTrace].redraw(g2d);
                currentTrace += 1;
            }
        }
    }

    /**
     * Updates all of the model traces set interval.
     */
    private void updateTraces() {
        if (_modelTime - _lastTraceUpdate > _traceInterval) {
            _lastTraceUpdate = _modelTime;
            int currentTrace = 0;

            for (int i = 0; i < _nNodes; i ++) {
                if (_nodes[i].traceVelocity()) { 
                    _traces[currentTrace].update(_nodes[i].getScalarVelocity());
                    currentTrace += 1;
                }
            }
        }
    }

    /**
     * Adjusts the time increment so that the node with the greatest velocity
     * does not move further than the maximum travel distance in the alotted
     * time.
     */
    private void computeTimeIncrement () {
        Double velocity = 0.0;
        Double maximumVelocity = 0.0;
        for (int i = 0; i < _nNodes; i ++) {
            if (! _nodes[i].isStatic()) {
                velocity = _nodes[i].getScalarVelocity();
                if (velocity > maximumVelocity) maximumVelocity = velocity;
            }
        }

        if (maximumVelocity == 0.0) {
            _timeIncrement = _maximumTimeIncrement;
        } else {
            _timeIncrement = _maximumTravelDistance / maximumVelocity;
            if (_timeIncrement > _maximumTimeIncrement) _timeIncrement = _maximumTimeIncrement;
        }
    }

    /**
     * Advances model time and adjust nodeID velocities and positions accordingly.
     * Model time is always advanced by an amount required to keep the instantaneous
     * travel distance of all nodes within the classes maximumTravelDistance.
     */
    public void advanceTime() {

       computeTimeIncrement();

       for (int i = 0; i < _nNodes; i ++) {
           _nodes[i].applyVelocity(_timeIncrement);
       }

       applyForces();
       _modelTime += _timeIncrement;
       updateTraces();
    }
    
    /**
     * Returns the current model time in seconds.
     * @return
     */
    public Double getTime() {
        return _modelTime;
    }

    /**
     * Returns the diameter of the smallest sphere required to encompass
     * every node in the model when that sphere is centered on the specified
     * vector.
     * @return
     */
    public Double getDiameter(Vector3D center) {
        Vector3D delta = new Vector3D();
        Double maximumDistance = 0.0;
        Double distance = 0.0;
        for (int node = 0; node < _nNodes; node ++) {
            delta.vectorSet(_nodes[node].getVectorPosition());
            delta.subtractVector(center);
            distance = delta.getAmplitude();
            if (distance > maximumDistance) maximumDistance = distance;
        }
        return maximumDistance * 2.0;
    }

     /**
     * Reterns a vector to the nodes contained in the model.
     * @return
     */
    public Vector3D getCenter() {
        Vector3D center = new Vector3D();
        Double averageX = 0.0;
        Double averageY = 0.0;
        Double averageZ = 0.0;
        for (int node = 0; node < _nNodes; node ++) {
           averageX += _nodes[node].getCoordinate(0);
           averageY += _nodes[node].getCoordinate(1);
           averageZ += _nodes[node].getCoordinate(2);
        }
        averageX /= _nNodes;
        averageY /= _nNodes;
        averageZ /= _nNodes;

        center.cartesianSet(averageX, averageY, averageZ);
        return center;
    }
}
